/**
 * Copyright 2013 Bhushan
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package org.s4j.signals.core;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.s4j.signals.exception.ConnectionException;
import org.s4j.signals.exception.NoSuchSignalException;
import org.s4j.signals.exception.NoSuchSlotException;
import org.s4j.signals.helper.ReflectionHelper;
import org.s4j.signals.helper.Validator;

/**
 * Primary orchestrator that will help connecting signals to slots. Provides utility methods to connect signals and
 * slots. Also provides mechanism to emit events from objects.
 * 
 * @author Bhushan
 * 
 */
public final class Bridge {
    // Contains all the sync subscriptions
    private static Map<Invocable, List<Invocable>> SyncConnectionsMap = new HashMap<Invocable, List<Invocable>>();
    // Contains all the async subscriptions
    private static Map<Invocable, List<Invocable>> AsyncConnectionsMap = new HashMap<Invocable, List<Invocable>>();
    // Synchronization object to help synchronize across requesters
    private static Object Synchronizer = new Object();

    /**
     * Emits a given signal. Invoking this method ensures that all the subscribers are notified of the change. Depending
     * on the subscription type, it can either be within the context of the method (Sync) or in a separate thread
     * (Async)
     * 
     * @param source
     *            object that will emit the signal
     * @param signal
     *            name of the signal declared as part of the object
     * @param parameters
     *            parameters that need to be passed to the destination
     * @throws NoSuchSignalException
     *             in case there was no signal of the specified name declared in the object
     */
    public static void emit(final Bridgeable source, final String signal, final Object... parameters)
            throws NoSuchSignalException {

        final SignalContainer signalContainer = ReflectionHelper.fetchSignal(source, signal);
        final List<Invocable> syncSubscribers = fetchSubscribers(ConnectionType.Sync, signalContainer);
        final List<Invocable> asyncSubscribers = fetchSubscribers(ConnectionType.Async, signalContainer);

        notifySubscribers(syncSubscribers, parameters);

        if (asyncSubscribers == null || asyncSubscribers.isEmpty()) {
            return;
        }

        Runnable runnable = new Runnable() {

            @Override
            public void run() {
                notifySubscribers(asyncSubscribers, parameters);
            }
        };

        // Notify in a separate thread and continue execution
        new Thread(runnable).start();
    }

    /**
     * Notifies a given set of subscribers. Uses the InvocationVisitor to to trigger an action that corresponds to the
     * subscriber.
     * 
     * @param subscribers
     *            a list of invocations that need to be made
     * @param parameters
     *            parameters that need to be passed to the invocations
     */
    private static void notifySubscribers(final List<Invocable> subscribers, final Object... parameters) {
        InvocationVisitor invoker = new InvocationVisitor();
        for (Invocable subscriber : subscribers) {
            subscriber.accept(invoker, parameters);
        }
    }

    /**
     * Connects a signal to a slot. Method assumes that the source and signal parameters refer to signals present in the
     * object.
     * 
     * @param source
     *            objects that will act as the source of notifications
     * @param signal
     *            the notification that will be fired from the source object
     * @param destination
     *            destination object that will receive the notifications
     * @param slot
     *            method that will be invoked when the signal is emitted.
     * @throws NoSuchSignalException
     *             in case the signal is not present in the source
     * @throws NoSuchSlotException
     *             in case the slot is not defined in the destination
     * @throws ConnectionException
     *             misc errors that were unforeseen
     */
    public static void connect(final Bridgeable source, final String signal, final Bridgeable destination,
            final String slot) throws NoSuchSignalException, NoSuchSlotException, ConnectionException {
        connect(source, signal, ConnectionType.Sync, destination, slot);
    }

    /**
     * Connects a signal to a slot. Method assumes that the source and signal parameters refer to signals present in the
     * object.
     * 
     * @param source
     *            objects that will act as the source of notifications
     * @param signal
     *            the notification that will be fired from the source object
     * @param type
     *            type of connection to be made.
     * @param destination
     *            destination object that will receive the notifications
     * @param slot
     *            method that will be invoked when the signal is emitted.
     * @throws NoSuchSignalException
     *             in case the signal is not present in the source
     * @throws NoSuchSlotException
     *             in case the slot is not defined in the destination
     * @throws ConnectionException
     *             misc errors that were unforeseen
     */
    public static void connect(final Bridgeable source, final String signal, final ConnectionType type,
            final Bridgeable destination, final String slot) throws NoSuchSignalException, NoSuchSlotException,
            ConnectionException {

        final SignalContainer signalContainer = ReflectionHelper.fetchSignal(source, signal);
        final SlotContainer slotContainer = ReflectionHelper.fetchSlot(destination, slot);

        Validator.validateConnection(signalContainer, slotContainer);
        addSubscription(type, signalContainer, slotContainer);
    }

    /**
     * Connects a signal to a signal. Method assumes that the source and signal parameters refer to signals present in
     * the object.
     * 
     * @param source
     *            objects that will act as the source of notifications
     * @param sourceSignal
     *            the notification that will be fired from the source object
     * @param destination
     *            destination object that will receive the notifications
     * @param destSignal
     *            method that will be invoked when the signal is emitted.
     * @throws NoSuchSignalException
     *             in case the signal is not present in the source
     * @throws NoSuchSlotException
     *             in case the slot is not defined in the destination
     * @throws ConnectionException
     *             misc errors that were unforeseen
     */
    public static void connectSignals(final Bridgeable source, final String sourceSignal, final Bridgeable destination,
            final String destSignal) throws NoSuchSignalException, NoSuchSlotException, ConnectionException {
        connectSignals(source, sourceSignal, ConnectionType.Sync, destination, destSignal);
    }

    /**
     * Connects a signal to a signal. Method assumes that the source and signal parameters refer to signals present in
     * the object.
     * 
     * @param source
     *            objects that will act as the source of notifications
     * @param sourceSignal
     *            the notification that will be fired from the source object
     * @param type
     *            type of connection that needs to be made
     * @param destination
     *            destination object that will receive the notifications
     * @param destSignal
     *            method that will be invoked when the signal is emitted.
     * @throws NoSuchSignalException
     *             in case the signal is not present in the source
     * @throws NoSuchSlotException
     *             in case the slot is not defined in the destination
     * @throws ConnectionException
     *             misc errors that were unforeseen
     */
    public static void connectSignals(final Bridgeable source, final String sourceSignal, final ConnectionType type,
            final Bridgeable destination, final String destSignal) throws NoSuchSignalException, NoSuchSlotException,
            ConnectionException {

        final SignalContainer sourceSignalContainer = ReflectionHelper.fetchSignal(source, sourceSignal);
        final SignalContainer destSignalContainer = ReflectionHelper.fetchSignal(destination, destSignal);

        Validator.validateConnection(sourceSignalContainer, destSignalContainer);
        addSubscription(type, sourceSignalContainer, destSignalContainer);
    }

    public static void disconnect(final Bridgeable source, final String signal, final ConnectionType type,
            final Bridgeable destination, final String slot) throws NoSuchSignalException, NoSuchSlotException,
            ConnectionException {

        final SignalContainer signalContainer = ReflectionHelper.fetchSignal(source, signal);
        final SlotContainer slotContainer = ReflectionHelper.fetchSlot(destination, slot);
        removeSubscription(type, signalContainer, slotContainer);
    }

    public static void disconnectSignals(final Bridgeable source, final String signal, final ConnectionType type,
            final Bridgeable destination, final String destSignal) throws NoSuchSignalException, NoSuchSlotException,
            ConnectionException {

        final SignalContainer signalContainer = ReflectionHelper.fetchSignal(source, signal);
        final SignalContainer destSignalContainer = ReflectionHelper.fetchSignal(destination, destSignal);
        removeSubscription(type, signalContainer, destSignalContainer);
    }

    private static void removeSubscription(final ConnectionType type, final Invocable source,
            final Invocable destination) {

        synchronized (Synchronizer) {
            List<Invocable> subscribers = fetchSubscribersUnSynchronized(type, source, false);
            if (subscribers != null && !subscribers.isEmpty()) {
                subscribers.remove(destination);
                // TODO: Do we need to remove the array list created as well
            }
        }
    }

    private static void addSubscription(final ConnectionType type, final Invocable source, final Invocable destination) {
        synchronized (Synchronizer) {
            fetchSubscribersUnSynchronized(type, source, true).add(destination);
        }
    }

    private static List<Invocable> fetchSubscribers(final ConnectionType type, final Invocable source) {
        List<Invocable> subscribers = null;
        synchronized (Synchronizer) {
            subscribers = fetchSubscribersUnSynchronized(type, source, false);
        }
        return subscribers;
    }

    private static List<Invocable> fetchSubscribersUnSynchronized(final ConnectionType type, final Invocable source,
            final boolean createIfAbsent) {

        List<Invocable> subscribers = null;
        switch (type) {
        case Sync:
            subscribers = SyncConnectionsMap.get(source);
            if (subscribers == null && createIfAbsent) {
                subscribers = new ArrayList<Invocable>();
                SyncConnectionsMap.put(source, subscribers);
            }
            break;
        case Async:
            subscribers = AsyncConnectionsMap.get(source);
            if (subscribers == null && createIfAbsent) {
                subscribers = new ArrayList<Invocable>();
                AsyncConnectionsMap.put(source, subscribers);
            }
            break;
        }

        return subscribers;
    }
}
